
#include <ROOT_PATH.h>

#include <Util/Basic/UGM/UGM.h>
#include <Util/Basic/StrAPI.h>
#include <Util/Basic/Image.h>
#include <Util/Basic/StrAPI.h>
#include <Util/Engine/SObj.h>
#include <Util/Engine/Scene.h>
#include <Util/Engine/TriMesh.h>
#include <Util/Engine/Plane.h>
#include <Util/Engine/CmptGeometry.h>
#include <Util/Engine/CmptLight.h>
#include <Util/Engine/CmptTransform.h>
#include <Util/Engine/CmptCamera.h>
#include <Util/Engine/AreaLight.h>
#include <Util/Engine/InfiniteAreaLight.h>
#include <Util/Engine/SphereLight.h>
#include <Util/Engine/AllBSDFs.h>
#include <Util/Engine/CmptMaterial.h>
#include "SceneLoader.h"

#ifndef TINYOBJLOADER_IMPLEMENTATION
#define TINYOBJLOADER_IMPLEMENTATION
#endif // !TINYOBJLOADER_IMPLEMENTATION

#define Cerr(x) std::cerr<< x <<std::endl;
#define Cout(x) std::cout<< x <<std::endl;
#define Cout_L(x) std::cout<< "---------------------------"<< x <<"---------------------------"<<std::endl;

#include <3rdParty/tiny_obj_loader.h>

using namespace Util;
using namespace Util::Basic;
using namespace Util::Engine;

bool LoadModel(Ptr<SObj> &rootnode, const std::string &filename, Ptr<Material> &material) {
	tinyobj::attrib_t attrib;
	std::vector<tinyobj::shape_t> shapes;
	std::vector<tinyobj::material_t> materials;
	std::string err;
	bool ret = tinyobj::LoadObj(&attrib, &shapes, &materials, &err, filename.c_str(), 0, true);

	if (!ret)
	{
		Cerr("Unable to load model");
		return false;
	}

	std::string obj = filename.substr(filename.rfind("/") + 1);
	obj = obj.substr(0, obj.rfind("."));
	Ptr<SObj> fileNode = SObj::New(rootnode, obj);

	for (size_t s = 0; s < shapes.size(); s++)
	{
		Ptr<SObj> meshNode = SObj::New(fileNode, shapes[s].name);
		int index_offset = 0;

		std::vector<uint> indices;
		std::vector<Point3> positions;
		std::vector<Normalf> normals;
		std::vector<Point2> texcoords;

		for (size_t f = 0; f < shapes[s].mesh.num_face_vertices.size(); f++)
		{
			Point2 t[3];
			Vec3 v[3], n[3];
			bool hasNormals = true;
			float vx, vy, vz, nx, ny, nz, tx, ty;
			int fv = shapes[s].mesh.num_face_vertices[f];
			for (int i = 0; i < fv; i++)
			{
				tinyobj::index_t idx = tinyobj::index_t(shapes[s].mesh.indices[index_offset + i]);

				vx = attrib.vertices[3 * idx.vertex_index + 0];
				vy = attrib.vertices[3 * idx.vertex_index + 1];
				vz = attrib.vertices[3 * idx.vertex_index + 2];

				v[i] = Vec3(vx, vy, vz);

				//Normals
				if (idx.normal_index != -1)
				{
					nx = attrib.normals[3 * idx.normal_index + 0];
					ny = attrib.normals[3 * idx.normal_index + 1];
					nz = attrib.normals[3 * idx.normal_index + 2];
				}
				else
				{
					hasNormals = false;
				}

				if (idx.texcoord_index != -1)
				{
					tx = attrib.texcoords[2 * idx.texcoord_index + 0];
					ty = 1.0f - attrib.texcoords[2 * idx.texcoord_index + 1];
				}
				else
				{
					//If model has no texture Coords then default to 0
					tx = ty = 0;
				}

				n[i] = Vec3(nx, ny, nz);
				t[i] = Point2(tx, ty);

				indices.push_back((uint)positions.size());
				positions.push_back(v[i]);
				normals.push_back(n[i]);
				texcoords.push_back(t[i]);

			}
			if (!hasNormals)
			{
				//If model has no normals then Generate flat normals
				Normalf flatNormal = (v[1] - v[0]).Cross(v[2] - v[0]).Normalize();
				int size = (int)normals.size();
				normals[size - 1] = normals[size - 2] = normals[size - 3] = flatNormal;
			}

			index_offset += fv;
		}

		Ptr<TriMesh> mesh = TriMesh::New(indices, positions, normals, texcoords);
		meshNode->AddComponent<CmptGeometry>(mesh);
		if (material != nullptr) {
			auto mat = CmptMaterial::New(meshNode, material);
		}
	}
	return true;
}

Ptr <Scene> LoadScene(const std::string &filename)
{
	std::ifstream in;
	in.open(filename, std::ifstream::in);
	if (in.fail()) return nullptr;
	std::string line;

	std::string SceneRoot = StrAPI::DelTailAfter(filename, '/') + '/';

	std::unordered_map<std::string, Ptr<Material>> materialMap;

	//Defaults
	auto rootnode = SObj::New(nullptr, "root");
	auto mesh = SObj::New(rootnode, "mesh");
	auto light = SObj::New(rootnode, "light");
	int materialCount = 0;
	bool cameraAdded = false;

	while (!in.eof())
	{
		std::getline(in, line);
		std::istringstream iss(line);
		std::string keyword = "";
		iss >> keyword;

		// skip comments
		if (keyword[0] == '#')
			continue;

		// name used for materials and meshes
		std::string name = "";

		//----------------------------------------------------------
		// Material
		//----------------------------------------------------------
		if (keyword == "material")
		{
			iss >> name;

			float noMetallic = 0;
			float noRoughness = 0;
			std::string albedoTexName = "None";
			std::string metallicTexName = "None";
			std::string roughnessTexName = "None";
			std::string metallicRoughnessTexName = "None";
			std::string normalTexName = "None";
			std::string tempTexName = "None";

			Vec4 albedo(1.0f, 1.0f, 1.0f, 0.0f);
			Vec4 params(0.0f, 1.0f, 0.0f, 0.0f);
			Vec4 texIDs(-1.0f, -1.0f, -1.0f, -1.0f);

			while (!in.eof())
			{
				std::getline(in, line);
				std::istringstream issTmp(line);
				issTmp >> keyword;

				if (line == "}")
					break;

				if (keyword == "name") issTmp >> name;
				if (keyword == "color") issTmp >> albedo.x >> albedo.y >> albedo.z;
				//if (keyword == "emission") issTmp >> emission.x >> emission.y >> emission.z;
				if (keyword == "materialType") issTmp >> albedo.w;
				if (keyword == "metallic") issTmp >> params.x;
				if (keyword == "roughness") issTmp >> params.y;
				if (keyword == "ior") issTmp >> params.z;
				//if (keyword == "transmittance") issTmp >> params.w;
				if (keyword == "temperature") issTmp >> params.w;

				if (keyword == "albedoTexture") issTmp >> albedoTexName;
				if (keyword == "metallicTexture") issTmp >> metallicTexName;
				if (keyword == "roughnessTexture") issTmp >> roughnessTexName;
				if (keyword == "metallicRoughnessTexture") issTmp >> metallicRoughnessTexName;
				if (keyword == "normalTexture") issTmp >> normalTexName;
				if (keyword == "noRoughness") issTmp >> noRoughness;
				if (keyword == "albedoTexture") issTmp >> tempTexName;
			}

			if (albedo.w == 0.0) {
				auto bsdf = BSDF_Frostbite::New(albedo, params.y, params.x);
				if (albedoTexName != "None") {
					bsdf->albedoTexture = Image::New(SceneRoot + albedoTexName);
				}
				if (normalTexName != "None") {
					bsdf->normalTexture = Image::New(SceneRoot + normalTexName);
				}
				if (metallicTexName != "None") {
					bsdf->metallicTexture = Image::New(SceneRoot + metallicTexName);
				}
				if (roughnessTexName != "None") {
					bsdf->roughnessTexture = Image::New(SceneRoot + roughnessTexName);
				}
				if (metallicRoughnessTexName != "None") {
					auto mrtex = Image::New(SceneRoot + metallicRoughnessTexName);
					int width = mrtex->GetWidth(); int height = mrtex->GetHeight();
					int channel = mrtex->GetChannel();
					bsdf->metallicTexture = Image::New(width, height, 1);
					bsdf->roughnessTexture = Image::New(width, height, 1);
					for (int i = 0; i < width; i++) {
						for (int j = 0; j < height; j++) {
							bsdf->metallicTexture->At(i, j, 0) = mrtex->At(i, j, 2);
							bsdf->roughnessTexture->At(i, j, 0) = mrtex->At(i, j, 1);
						}
					}
				}

				if (materialMap.find(name) == materialMap.end()) // New material
					materialMap[name] = bsdf;
			}
			else {
				auto bsdf = BSDF_FrostedGlass::New(albedo, params.y, params.z);
				if (albedoTexName != "None") {
					bsdf->colorTexture = Image::New(SceneRoot + albedoTexName);
				}
				if (normalTexName != "None") {
					bsdf->normalTexture = Image::New(SceneRoot + normalTexName);
				}
				if (roughnessTexName != "None") {
					bsdf->roughnessTexture = Image::New(SceneRoot + roughnessTexName);
				}
				if (metallicRoughnessTexName != "None") {
					auto mrtex = Image::New(SceneRoot + metallicRoughnessTexName);
					int width = mrtex->GetWidth(); int height = mrtex->GetHeight();
					int channel = mrtex->GetChannel();
					bsdf->roughnessTexture = Image::New(width, height, 1);

					for (int i = 0; i < width; i++)
						for (int j = 0; j < height; j++)
							bsdf->roughnessTexture->At(i, j, 0) = mrtex->At(i, j, 1);

				}

				if (materialMap.find(name) == materialMap.end()) // New material
					materialMap[name] = bsdf;
			}
		}
		//----------------------------------------------------------
		// Light
		//----------------------------------------------------------
		if (keyword == "light")
		{
			Point3 position, v1, v2;
			Vec3 emission, radiusAreaType, u, v;
			std::string light_type = "None";

			while (!in.eof())
			{
				std::getline(in, line);
				std::istringstream issTmp(line);
				issTmp >> keyword;

				if (line == "}")
					break;

				if (keyword == "position") issTmp >> position.x >> position.y >> position.z;
				if (keyword == "emission") issTmp >> emission.x >> emission.y >> emission.z;
				if (keyword == "radius") issTmp >> radiusAreaType.x;
				if (keyword == "v1") issTmp >> v1.x >> v1.y >> v1.z;
				if (keyword == "v2") issTmp >> v2.x >> v2.y >> v2.z;
				if (keyword == "type") issTmp >> light_type;
			}

			if (light_type == "Quad")
			{
				u = v1 - position;
				v = v2 - position;

				auto sobj_AreaLight = SObj::New(light, "area light");
				auto areaLight = AreaLight::New(RGBf(emission), 1.0, u.Norm(), v.Norm());
				auto cmptLight = CmptLight::New(sobj_AreaLight, areaLight);
				auto lightTransform = CmptTransform::New(sobj_AreaLight);
				auto target = position + v.Cross(u);
				lightTransform->LookAt(position, target, v.Normalize());
			}
			else if (light_type == "Sphere")
			{
				auto sobj_SphereLight = SObj::New(light, "sphere light");
				auto sphereLight = SphereLight::New(RGBf(emission), 1.0, radiusAreaType.x);
				auto cmptLight = CmptLight::New(sobj_SphereLight, sphereLight);
				auto lightTransform = CmptTransform::New(sobj_SphereLight);
				lightTransform->SetPosition(position);
			}
		}

		//----------------------------------------------------------
		// Camera
		//----------------------------------------------------------
		if (keyword == "Camera")
		{
			Point3 position, target;
			float fov;

			while (!in.eof())
			{
				std::getline(in, line);
				std::istringstream issTmp(line);
				issTmp >> keyword;

				if (line == "}")
					break;

				if (keyword == "position") issTmp >> position.x >> position.y >> position.z;
				if (keyword == "lookAt") issTmp >> target.x >> target.y >> target.z;
				if (keyword == "fov") issTmp >> fov;
			}

			auto sobj_Camera = SObj::New(rootnode, "camera");
			auto cmptCamera = CmptCamera::New(sobj_Camera, fov);
			auto cameraTransform = CmptTransform::New(sobj_Camera);
			cameraTransform->LookAt(position, target);
			cameraAdded = true;
		}

		//----------------------------------------------------------
        // Renderer
        //----------------------------------------------------------
		if (line == "Renderer")
		{
			std::string envMap = "None";

			while (!in.eof())
			{
				std::getline(in, line);
				std::istringstream issTmp(line);
				issTmp >> keyword;

				if (line == "}")
					break;

				if (keyword == "envMap") issTmp >> envMap;

			}

			if (envMap != "None")
			{
				auto lightObj = SObj::New(rootnode, "envmap");
				lightObj->AddComponent<CmptLight>(InfiniteAreaLight::New(Image::New(SceneRoot + envMap)));
			}
			else
			{
				auto lightObj = SObj::New(rootnode, "background lighting");
				lightObj->AddComponent<CmptLight>(InfiniteAreaLight::New(nullptr, Vec3(1.0), 0.35));
			}

		}

		//----------------------------------------------------------
		// Mesh
		//----------------------------------------------------------
		if (line == "mesh")
		{
			std::string meshPath;
			Ptr<Material> material(nullptr);
			float materialId = 0.0f; // Default Material ID
			while (!in.eof())
			{
				std::getline(in, line);
				std::istringstream issTmp(line);
				issTmp >> keyword;

				if (line == "}")
					break;

				std::string path = "";

				if (keyword == "file")
				{
					issTmp >> path;
					meshPath = SceneRoot + path;
				}

				if (keyword == "material")
				{
					issTmp >> path;

					// look up material in dictionary
					if (materialMap.find(path) != materialMap.end())
					{
						material = materialMap[path];
					}
					else
					{
						std::cerr << "Could not find material " << std::endl;
					}
				}
			}
			if (!meshPath.empty())
			{

				Cout("Loading Model: " << meshPath.c_str());
				if (!LoadModel(mesh, meshPath, material))
				{
					return false;
				}
			}
		}
	}
	if (!cameraAdded) {
		auto sobj_Camera = SObj::New(rootnode, "camera");
		auto cmptCamera = CmptCamera::New(sobj_Camera, 35.f);
		auto cameraTransform = CmptTransform::New(sobj_Camera);
		cameraTransform->LookAt(Vec3(0.0, 0.0, 0.0), Vec3(0.0, 0.0, -1));
		cameraAdded = true;
	}

	

	return Scene::New(rootnode);
}